<?php
/**
 * Created by PhpStorm.
 * User: zengdeshun
 * Date: 2018/5/12
 * Time: 14:10
 * descript:火币平台接口类
 */

namespace platform\driver\binance;

use think\Log;
use Workerman\Connection\AsyncTcpConnection;
use platform\driver\Driver;
use exception\Platform as PlatformException;
use exception\Binance as BinanceException;
use exception\System as SystemException;


class Binance extends Driver
{
    /**
     * 币安实例
     * @var object
     */
    private $binanceApi;

    /**
     * 订阅端点容器
     * @var array
     */
    public $subscriptions;

    /**
     * 币安原始接口
     * @var string
     */
    private $_apiStream = 'wss://stream.binance.com:9443/ws/';

    /**
     * 交易接口
     * @var string
     */
    private $api = '';

    /**
     * API 访问密钥
     * @var string
     */
    private $accessKey = '';

    /**
     * 签名密钥
     * @var string
     */
    private $secretKey = '';

    /**
     * api请求路径
     * @var string
     */
    private $apiMethod = '';

    /**
     * webSocket接口地址
     * @var string
     */
    private $stream = '';

    public function __construct($param = [])
    {
        $config = file_get_contents(dirname(__FILE__) . '/config.json');
        $config = json_decode($config, true);
        $this->api = $config['api'];
        if (isset($param['access_key']) && isset($param['secret_key'])) {
            $this->accessKey = $param['access_key'];
            $this->secretKey = $param['secret_key'];
        } else {
            $this->accessKey = $config['access_key'];
            $this->secretKey = $config['secret_key'];
        }
        $this->stream = $config['stream'];
        $this->binanceApi = new \Binance\API($this->accessKey, $this->secretKey);
        $this->binanceApi = new RateLimiter($this->binanceApi);
    }

    /**
     * 获取完整的接口信息包括接口限制 币种等
     * @return array
     * @throws \Exception
     */
    public function symbols()
    {
        $data = $this->binanceApi->exchangeInfo();
        if (!is_array($data)) {
            $data = json_decode($data, true);
        }
        if(isset($data['code'])) {
            if (array_key_exists($data['msg'], $this->_errorMsg)) {
                $errorCode = $this->_errorMsg[$data['msg']];
                Log::record('币安请求接口失败，错误码' . $errorCode . '错误信息' . $errorCode);
                exception(BinanceException::getErrorMsg($errorCode), $errorCode, 'exception\Binance');
            } else {
                if (array_key_exists($data['code'], $this->_errorCode)) {
                    $errorCode = $this->_errorCode[$data['code']];
                    Log::record('币安请求接口失败，错误码' . $errorCode . '错误信息' . $errorCode);
                    exception(BinanceException::getErrorMsg($errorCode), $errorCode, 'exception\Binance');
                }
                exception($data['msg'], BinanceException::E_UNKNOWN_ERROR, 'exception\Binance');
            }
        }
        if (empty($data)) { //IP或者账号被禁止 记录日志 发送通知
            exception(PlatformException::getErrorMsg(PlatformException::E_DEFAULT), PlatformException::E_DEFAULT, 'exception\Platform');
        }
        return $data;
    }

    /**
     * 获取用户资产
     * @return array
     * @throws \Exception
     */
    public function balance()
    {
        $data = $this->binanceApi->balance();
        if (!is_array($data)) {
            $data = json_decode($data, true);
        }

        if(isset($data['code'])) {
            if (array_key_exists($data['msg'], $this->_errorMsg)) {
                $errorCode = $this->_errorMsg[$data['msg']];
                Log::record('币安请求接口失败，错误码' . $errorCode . '错误信息' . $errorCode);
                exception(BinanceException::getErrorMsg($errorCode), $errorCode, 'exception\Binance');
            } else {
                if (array_key_exists($data['code'], $this->_errorCode)) {
                    $errorCode = $this->_errorCode[$data['code']];
                    Log::record('币安请求接口失败，错误码' . $errorCode . '错误信息' . $errorCode);
                    exception(BinanceException::getErrorMsg($errorCode), $errorCode, 'exception\Binance');
                }
                exception($data['msg'], BinanceException::E_UNKNOWN_ERROR, 'exception\Binance');
            }
        }
        $balance = [];
        foreach ($data['balances'] as $obj) {
            $asset = $obj['asset'];
            $balance[$asset] = [
                'available' => $obj['free'],
                'onOrder'   => $obj['locked'],
            ];
        }
        return $balance;
    }

    /**
     * @param string $side  交易类型"BUY" or "SELL"
     * @param string $symbol 币种
     * @param $quantity 数量
     * @param $price 价格
     * @param string $type 订单类型 LIMIT  MARKET STOP_LOSS STOP_LOSS_LIMIT TAKE_PROFIT TAKE_PROFIT_LIMIT  LIMIT_MAKER
     * @param array $flags 额外参数
     * @param bool $test 是否是测试接口
     * @return array
     * @throws \Exception
     */
    public function buy(...$data)
    {
        list($side, $symbol, $quantity, $price) = $data;
        $type  = $data[4] ?? 'LIMIT';
        $flags = $data[5] ?? [];
        $test  = $data[6] ?? false;


        $this->apiMethod  = 'v3/order';
        $params = [
            'symbol' => $symbol,
            'side' => strtoupper($side),
            'type' => $type,
            //'timeInForce' => 'GTC',
            'quantity' => $quantity,
            //'price' => $price,
            'recvWindow' => '60000',
        ];
        if ($type == 'LIMIT' || $type == 'STOP_LOSS_LIMIT' || $type == 'TAKE_PROFIT_LIMIT') {
            $params['timeInForce'] = 'GTC';
            $params['price'] = $price;
            if (isset($flags['stopPrice'])) {
                $params['stopPrice'] = $flags['stopPrice'];
            }
        }
        $data = $this->shellPost($params);
        return $data['orderId'];
    }

    /**
     * 用shell 命令post 请求接口
     * @param $params
     * @return mixed|string
     * @throws \Exception
     */
    public function shellPost($params)
    {
        $ts = microtime(true) * 1000;
        $params['timestamp'] = number_format($ts, 0, '.', '');
        $query = http_build_query($params, '', '&');
        $signature = hash_hmac('sha256', $query, $this->secretKey);
        $endpoint = $this->api . $this->apiMethod . '?' . $query . '&signature=' . $signature;
        $response = shell_exec("curl -H 'X-MBX-APIKEY: " .  $this->accessKey . "' -X POST '" . $endpoint . "'");
        if (!is_array($response)) {
            $response = json_decode($response, true);
        }
        if(isset($response['code'])) {
            if (array_key_exists($response['msg'], $this->_errorMsg)) {
                $errorCode = $this->_errorMsg[$response['msg']];
                Log::record('币安请求接口失败，错误码' . $errorCode . '错误信息' . $errorCode);
                exception(BinanceException::getErrorMsg($errorCode), $errorCode, 'exception\Binance');
            } else {
                if (array_key_exists($response['code'], $this->_errorCode)) {
                    $errorCode = $this->_errorCode[$response['code']];
                    Log::record('币安请求接口失败，错误码' . $errorCode . '错误信息' . $errorCode);
                    exception(BinanceException::getErrorMsg($errorCode), $errorCode, 'exception\Binance');
                }
                exception($response['msg'], BinanceException::E_UNKNOWN_ERROR, 'exception\Binance');
            }
        }
        if (empty($response)) { //IP或者账号被禁止 记录日志 发送通知
            exception(PlatformException::getErrorMsg(PlatformException::E_DEFAULT), PlatformException::E_DEFAULT, 'exception\Platform');
        }

        return $response;
    }

    /**
     * 限价卖
     * @param string $symbol
     * @param $quantity
     * @param $price
     * @param string $type
     * @param array $flags
     * @return mixed
     * @throws \Exception
     */
    public function sell(string $symbol, $quantity, $price, string $type = "LIMIT", array $flags = [])
    {
        $data = $this->binanceApi->sell($symbol, $quantity, $price, $type, $flags);
        if (empty($data)) { //IP或者账号被禁止 记录日志 发送通知
            Log::record('币安下单失败，错误码' . $data['code'] . '错误信息' . $data['msg']);
            exception(PlatformException::getErrorMsg(PlatformException::E_ORDER_ERROR, 'OK'), PlatformException::E_ORDER_ERROR, 'exception\Platform');
        }
        if (!is_array($data)) {
            $data = json_decode($data, true);
        }
        if (array_key_exists($data['msg'], $this->_errorMsg)) {
            $errorCode = $this->_errorMsg[$data['msg']];
            Log::record('币安请求接口失败，错误码' . $errorCode . '错误信息' . $errorCode);
            exception(BinanceException::getErrorMsg($errorCode), $errorCode, 'exception\Binance');
        } else {
            if (array_key_exists($data['code'], $this->_errorCode)) {
                $errorCode = $this->_errorCode[$data['code']];
                Log::record('币安请求接口失败，错误码' . $errorCode . '错误信息' . $errorCode);
                exception(BinanceException::getErrorMsg($errorCode), $errorCode, 'exception\Binance');
            }
            exception($data['msg'], BinanceException::E_UNKNOWN_ERROR, 'exception\Binance');
        }
        return $data['orderId'];
    }

    /**
     * 市价卖
     * @param string $symbol
     * @param $quantity
     * @param array $flags
     * @return mixed
     * @throws \Exception
     */
    public function marketSell(string $symbol, $quantity, array $flags = [])
    {
        $data = $this->binanceApi->marketSell($symbol, $quantity, $flags);
        if (empty($data)) { //IP或者账号被禁止 记录日志 发送通知
            exception(PlatformException::getErrorMsg(PlatformException::E_ORDER_ERROR, 'OK'), PlatformException::E_ORDER_ERROR, 'exception\Platform');
        }
        if (!is_array($data)) {
            $data = json_decode($data, true);
        }
        if(isset($data['code'])) {
            if (array_key_exists($data['msg'], $this->_errorMsg)) {
                $errorCode = $this->_errorMsg[$data['msg']];
                Log::record('币安请求接口失败，错误码' . $errorCode . '错误信息' . $errorCode);
                exception(BinanceException::getErrorMsg($errorCode), $errorCode, 'exception\Binance');
            } else {
                if (array_key_exists($data['code'], $this->_errorCode)) {
                    $errorCode = $this->_errorCode[$data['code']];
                    Log::record('币安请求接口失败，错误码' . $errorCode . '错误信息' . $errorCode);
                    exception(BinanceException::getErrorMsg($errorCode), $errorCode, 'exception\Binance');
                }
                exception($data['msg'], BinanceException::E_UNKNOWN_ERROR, 'exception\Binance');
            }
        }
        return $data['orderId'];
    }

    /***
     * 订单取消
     * @param $symbol
     * @param $orderid
     * @return array
     * @throws \Exception
     */
    public function cancel(...$data)
    {
        list($symbol, $orderid) = $data;
        $data = $this->binanceApi->cancel($symbol, $orderid);
        if (empty($data)) { //IP或者账号被禁止 记录日志 发送通知
            exception(PlatformException::getErrorMsg(PlatformException::E_CANCEL_ERROR, 'OK'), PlatformException::E_CANCEL_ERROR, 'exception\Platform');
        }
        if (!is_array($data)) {
            $data = json_decode($data, true);
        }
        if(isset($data['code'])) {
            if (array_key_exists($data['msg'], $this->_errorMsg)) {
                $errorCode = $this->_errorMsg[$data['msg']];
                Log::record('币安请求接口失败，错误码' . $errorCode . '错误信息' . $errorCode);
                exception(BinanceException::getErrorMsg($errorCode), $errorCode, 'exception\Binance');
            } else {
                if (array_key_exists($data['code'], $this->_errorCode)) {
                    $errorCode = $this->_errorCode[$data['code']];
                    Log::record('币安请求接口失败，错误码' . $errorCode . '错误信息' . $errorCode);
                    exception(BinanceException::getErrorMsg($errorCode), $errorCode, 'exception\Binance');
                }
                exception($data['msg'], BinanceException::E_UNKNOWN_ERROR, 'exception\Binance');
            }
        }
        return $data;
    }

    /**
     * 查询订单状态
     * @param $symbol
     * @param $orderid
     * @return array
     * @throws \Exception
     */
    public function orderStatus($symbol, $orderid)
    {
        $data = $this->binanceApi->orderStatus($symbol, $orderid);
        if (empty($data)) { //IP或者账号被禁止 记录日志 发送通知
            exception(PlatformException::getErrorMsg(PlatformException::E_DEFAULT), PlatformException::E_DEFAULT, 'exception\Platform');
        }
        if (!is_array($data)) {
            $data = json_decode($data, true);
        }
        if(isset($data['code'])) {
            if (array_key_exists($data['msg'], $this->_errorMsg)) {
                $errorCode = $this->_errorMsg[$data['msg']];
                Log::record('币安请求接口失败，错误码' . $errorCode . '错误信息' . $errorCode);
                exception(BinanceException::getErrorMsg($errorCode), $errorCode, 'exception\Binance');
            } else {
                if (array_key_exists($data['code'], $this->_errorCode)) {
                    $errorCode = $this->_errorCode[$data['code']];
                    Log::record('币安请求接口失败，错误码' . $errorCode . '错误信息' . $errorCode);
                    exception(BinanceException::getErrorMsg($errorCode), $errorCode, 'exception\Binance');
                }
                exception($data['msg'], BinanceException::E_UNKNOWN_ERROR, 'exception\Binance');
            }
        }
        return $data;
    }

    /**
     * 获取订单详情
     * @param $symbol
     * @param $fromOrderId
     * @param int $limit
     * @return array
     * @throws \Exception
     */
    public function order(...$data)
    {
        list($fromOrderId, $symbol) = $data;
        $limit = $data[2] ?? 500;
        $data = $this->binanceApi->orders($symbol, $limit, $fromOrderId);
        if (empty($data)) { //IP或者账号被禁止 记录日志 发送通知
            exception(PlatformException::getErrorMsg(PlatformException::E_DEFAULT), PlatformException::E_DEFAULT, 'exception\Platform');
        }
        if (!is_array($data)) {
            $data = json_decode($data, true);
        }
        if(isset($data['code'])) {
            if (array_key_exists($data['msg'], $this->_errorMsg)) {
                $errorCode = $this->_errorMsg[$data['msg']];
                Log::record('币安请求接口失败，错误码' . $errorCode . '错误信息' . $errorCode);
                exception(BinanceException::getErrorMsg($errorCode), $errorCode, 'exception\Binance');
            } else {
                if (array_key_exists($data['code'], $this->_errorCode)) {
                    $errorCode = $this->_errorCode[$data['code']];
                    Log::record('币安请求接口失败，错误码' . $errorCode . '错误信息' . $errorCode);
                    exception(BinanceException::getErrorMsg($errorCode), $errorCode, 'exception\Binance');
                }
                exception($data['msg'], BinanceException::E_UNKNOWN_ERROR, 'exception\Binance');
            }
        }
        return $data;
    }

    /**
     * webSocket订阅24小时内所有ticker数据
     * @param $symbol
     * @param $callback
     * @return bool
     */
    public function tickerArrWebSocket($callback)
    {
        $this->webSocketClient($this->stream . '!ticker@arr', $callback);
        return true;
    }

    /**
     * webSocket接口统一请求客户端
     * @param $url
     * @param $data
     * @return bool
     */
    protected function webSocketClient($url, $callback)
    {
        try {
            $con = new AsyncTcpConnection($url);
        } catch (\Exception $e) {
            Log::record('请求webSocket接口出错 ' . $url . $e->getMessage());
        }

        $con->transport = 'ssl'; // 设置以ssl加密方式访问，使之成为wss
        $con->onConnect = function($con) {

        };

        $con->onMessage = function($con, $data) use ($callback) {
            call_user_func_array($callback, array($data));
        };

        $con->connect();
        return true;
    }

    /**
     * 获取行情[事件循环]
     */
    public function tickerEventLoop(array $symbols, callable $callback): void
    {
        $loop = \React\EventLoop\Factory::create();
        $react = new \React\Socket\Connector($loop);
        $connector = new \Ratchet\Client\Connector($loop, $react);

        foreach ($symbols as $symbol) {
            $symbol = strtoupper(str_replace('_', '', $symbol));
            $endpoint = strtolower($symbol) . '@ticker';

            $this->subscriptions[$endpoint] = true;
            $connector($this->_apiStream . $endpoint)->then(function ($ws) use ($callback, $symbol, $loop, $endpoint) {
                $ws->on('message', function ($data) use ($ws, $loop, $callback, $endpoint) {
                    if ($this->subscriptions[$endpoint] === false) {
                        $loop->stop();
                        return;
                    }

                    $json = json_decode($data, true);
                    call_user_func($callback, $json);
                });
                $ws->on('close', function ($code = null, $reason = null) use ($symbol, $loop) {
                    // WPCS: XSS OK.
                    echo "ticker ({$symbol}) WebSocket Connection closed! ({$code} - {$reason})" . PHP_EOL;
                    $loop->stop();
                    exit();
                });
            }, function ($e) use ($loop, $symbol) {
                // WPCS: XSS OK.
                echo "ticker Could not connect: {$e->getMessage()}" . PHP_EOL;
                $loop->stop();
                exit();
            });
        }

        $loop->run();
    }

    /**
     * 请求市场深度
     */
    public function depth(array $symbols, callable $callback): void
    {
        $this->binanceApi->depthCache($symbols, $callback);
    }

    /**
     * 错误码映射
     * @var array
     */
    private $_errorCode =[
        '-1000' => '26001', //未知错误
        '-1001' => '26002', //内部错误; 无法处理您的请求。请再试一次
        '-1002' => '26003',
        '-1003' => '26004',
        '-1006' => '26005',
        '-1007' => '26006',
        //'-1013'  =>  '26007',
        '-1014' => '26008',
        '-1015' => '26009',
        '-1016' => '26010',
        '-1020' => '26011',
        '-1021' => '26012',
        '-2022' => '26013',
        '-2011' => '26023', //CANCEL_REJECTED
        '-1100' => '26024', //ILLEGAL_CHARS
        '-1101' => '26025', //TOO_MANY_PARAMETERS
        '-1102' => '26026', //MANDATORY_PARAM_EMPTY_OR_MALFORMED
        '-1103' => '26027', //UNKNOWN_PARAM
        '-1104' => '26028', //UNREAD_PARAMETERS
        '-1105' => '26066', //PARAM_EMPTY
        '-1106' => '26029', //PARAM_NOT_REQUIRED
        '-1111' => '26030', //BAD_PRECISION
        '-1112' => '26031', //NO_DEPTH
        '-1114' => '26032', //TIF_NOT_REQUIRED
        '-1115' => '26033', //INVALID_TIF
        '-1116' => '26034', //INVALID_ORDER_TYPE
        '-1117' => '26035', //INVALID_SIDE
        '-1118' => '26036', //EMPTY_NEW_CL_ORD_ID
        '-1119' => '26037', //EMPTY_ORG_CL_ORD_ID
        '-1120' => '26038', //BAD_INTERVAL
        '-1121' => '26039', //BAD_SYMBOL
        '-1125' => '26040', //INVALID_LISTEN_KEY
        '-1127' => '26041', //MORE_THAN_XX_HOURS
        '-1128' => '26042', //OPTIONAL_PARAMS_BAD_COMBO
        '-1130' => '26043', //INVALID_PARAMETER
        '-2010' => '26044', //NEW_ORDER_REJECTED
        '-2013' => '26045', //NO_SUCH_ORDER
        '-2014' => '26046', //BAD_API_KEY_FMT
        '-2015' => '26047', //REJECTED_MBX_KEY
        '-2016' => '26048', //NO_TRADING_WINDOW
    ];

    private $_errorMsg = [
        'Filter failure: PRICE_FILTER'             => '26015', //price is too high, too low, and/or not following the tick size rule for the symbol
        'Filter failure: LOT_SIZE'                 => '26016', //quantity is too high, too low, and/or not following the step size rule for the symbol.
        'Filter failure: MIN_NOTIONAL'             => '26017', //price * quantity is too low to be a valid order for the symbol.
        'Filter failure: MAX_NUM_ORDERS'           => '26018', //Account has too many open orders on the symbol.
        'Filter failure: MAX_ALGO_ORDERS'          => '26019', //Account has too many open stop loss and/or take profit orders on the symbol.
        'Filter failure: EXCHANGE_MAX_NUM_ORDERS'  => '26020', //Account has too many open orders on the exchange.
        'Filter failure: EXCHANGE_MAX_ALGO_ORDERS' => '26021', //Account has too many open stop loss and/or take profit orders on the exchange.
        'Filter failure: ICEBERG_PARTS'            => '26022', //Iceberg order would break into too many parts; icebergQty is too small.
        'Unknown order sent.'                      => '26049', //The order (by either orderId, clOrdId, origClOrdId) could not be found
        'Duplicate order sent.'                    => '26050', //The clOrdId is already in use
        'Market is closed.'                        => '26051', //The symbol is not trading
        'Account has insufficient balance for requested action.'      => '26052', //Not enough funds to complete the action
        'Market orders are not supported for this symbol.'            => '26053', //MARKET is not enabled on the symbol
        'Iceberg orders are not supported for this symbol.'           => '26054', //icebergQty is not enabled on the symbol
        'Stop loss orders are not supported for this symbol.'         => '26055', //STOP_LOSS is not enabled on the symbol
        'Stop loss limit orders are not supported for this symbol.'   => '26056', //STOP_LOSS_LIMIT is not enabled on the symbol
        'Take profit orders are not supported for this symbol.'       => '26057', //TAKE_PROFIT is not enabled on the symbol
        'Take profit limit orders are not supported for this symbol.' => '26058', //TAKE_PROFIT_LIMIT is not enabled on the symbol
        'Price * QTY is zero or less.'                                => '26059', //price * quantity is too low
        'IcebergQty exceeds QTY.'                                     => '26060', //icebergQty must be less than the order quantity
        'This action disabled is on this account.'                    => '26061', //Contact customer support; some actions have been disabled on the account.
        'Unsupported order combination'                               => '26062', //The orderType, timeInForce, stopPrice, and/or icebergQty combination isn't allowed.
        'Order would trigger immediately.'                            => '26063', //The order's stop price is not valid when compared to the last traded price.
        'Cancel order is invalid. Check origClOrdId and orderId.'     => '26064', //No origClOrdId or orderId was sent in.
        'Order would immediately match and take.'                     => '26065'  //LIMIT_MAKER order type would immediately match and trade, and not be a pure maker order.
    ];

    /**
     * REST方式获取行情
     * @return array
     */
    public function restTicker()
    {
        $data = [];
        try {
            $data = $this->binanceApi->prevDay();
        } catch (\Exception $e) {
            Log::record('初始化币安请求行情数据出错' . $e->getMessage());
        }

        return $data;
    }

    /**
     * REST方式获取深度
     * @return array
     */
    public function restDepth($symbol)
    {
        $data = [];
        try {
            $data = $this->binanceApi->depth(strtoupper(str_replace('_', '', $symbol)));
        } catch (\Exception $e) {
            Log::record('初始化币安请求深度数据出错' . $e->getMessage());
        }

        return $data;
    }
}